package rpg.engine.client;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import rpg.client.Instruction;
import rpg.client.Instruction.Type;
import rpg.client.model.Actor;
import rpg.client.model.Actor.Gender;
import rpg.client.model.ActorInfo;
import rpg.client.model.ClientMap;
import rpg.client.model.Dialog;
import rpg.client.model.Dialog.Mode;
import rpg.client.model.Facing;
import rpg.client.model.GameEvent;
import rpg.client.model.GameObject;
import rpg.client.model.ObjectPosition;
import rpg.client.model.Position;
import rpg.engine.client.cache.PlayerInfo;
import rpg.engine.model.DialogInfo;
import rpg.engine.model.GameEffect;
import rpg.engine.model.MagicArea;
import rpg.engine.model.Mover;
import rpg.engine.model.Story;
import rpg.engine.model.When;
import rpg.util.Objects;

public class ClientEngine {
	private static boolean nearby(final Position position, final Position from) {
		if (!position.getMapID().equals(from.getMapID())) {
			return false;
		}
		final float dx = Math.abs(position.getX() - from.getX());
		if (dx > 1) {
			return false;
		}
		final float dy = Math.abs(position.getY() - from.getY());
		return (dy <= 1);
	}

	private final WorldCacheApi worldCache;

	ClientEngine(final WorldCacheApi worldCache) {
		super();
		this.worldCache = Objects.requireNonNull(worldCache,
				"ClientEngine.worldCache");
	}

	List<GameEvent> addPlayer(final Actor player) {
		assert (player != null);
		worldCache.addPlayer(player);
		return startStory(player.getId(), null);
	}

	private List<Instruction> canWeTalk(final PlayerInfo playerInfo,
			final NpcInfo npcInfo) {
		final Map<String, String> map = new HashMap<String, String>();

		// Do the other Active Stories
		for (final String storyID : playerInfo.getActiveStories()) {
			canWeTalk(playerInfo, npcInfo, storyID, map);
		}

		final List<Instruction> list = new ArrayList<Instruction>();

		for (final Map.Entry<String, String> entry : map.entrySet()) {
			list.add(Instruction.startChat(playerInfo.getActorId(), entry
					.getKey(), entry.getValue(), npcInfo.getActor().getName()));
		}

		return list;
	}

	void canWeTalk(final PlayerInfo playerInfo, final NpcInfo npcInfo,
			final String storyID, final Map<String, String> mapStoryToChat) {
		debug(storyID);
		final Collection<DialogInfo> dialogs = npcInfo.getIntroDialogs(storyID);
		if (dialogs == null) {
			return;
		}
		for (final DialogInfo di : dialogs) {
			final Dialog d = di.getDialog();
			if (d.getMode() != Mode.TALKING) {
				continue;
			}
			if (playerInfo.isChatDone(storyID, d.getId())) {
				continue;
			}

			// Check the whens...
			boolean allPassed = true;
			for (final When when : di.getWhens()) {
				switch (when.getType()) {
				case ACT_IS:
					if (!when.getTargetID().equals(playerInfo.getAct(storyID))) {
						allPassed = false;
					}
					break;
				case INVENTORY_HAS:
					if (!playerInfo.getObjectPositions().containsKey(
							when.getTargetID())) {
						allPassed = false;
					}
					break;
				case IS_WEARING:
					final ObjectPosition op = playerInfo.getObjectPositions()
							.get(when.getTargetID());
					if (op != ObjectPosition.WEARING) {
						allPassed = false;
					}
					break;
				case TOKEN_MISSING:
					if (playerInfo.hasToken(when.getStoryID(),
							when.getTargetID())) {
						allPassed = false;
					}
					break;
				case ALWAYS:
					break;
				default:
					throw new UnsupportedOperationException(when
							+ " not handled!");
				}

				if (!allPassed) {
					break;
				}
			}

			if (allPassed) {
				mapStoryToChat.put(storyID, d.getId());
			}
		}
	}

	private MagicArea checkMagicAreas(final Position toPosition,
			final List<MagicArea> magicAreas) {
		MagicArea triggered = null;

		for (final MagicArea ma : magicAreas) {
			final Position from = ma.getFrom();
			if (!toPosition.getMapID().equals(from.getMapID())) {
				continue;
			}
			if (toPosition.getX() < from.getX()) {
				continue;
			}
			if (toPosition.getY() < from.getY()) {
				continue;
			}
			final Position to = ma.getTo();
			if (toPosition.getX() > to.getX()) {
				continue;
			}
			if (toPosition.getY() > to.getY()) {
				continue;
			}

			triggered = ma;
			break;
		}

		return triggered;
	}

	private void debug(final Object o) {
		// System.out.println("ClientEngine: " + o);
	}

	ActorInfo getCharacter(final String playerID) {
		final PlayerInfo info = worldCache.getPlayerInfo(playerID);
		final ActorInfo.Builder aib = new ActorInfo.Builder();
		aib.actor(info.getActor());
		aib.facing(info.getFacing());
		aib.position(info.getPosition());

		for (final Map.Entry<String, ObjectPosition> entry : info
				.getObjectPositions().entrySet()) {
			final GameObjectInfo goi = worldCache.getObject(entry.getKey());
			if (entry.getValue() == ObjectPosition.INVENTORY) {
				aib.inventory(new GameObject(goi));
			} else if (entry.getValue() == ObjectPosition.WEARING) {
				aib.wearing(new GameObject(goi));
			}
		}

		return aib.build();
	}

	List<Instruction> getPossibleInstructions(final String playerID) {
		final List<Instruction> list = new ArrayList<Instruction>();

		// See if there are any movers nearby to use
		final PlayerInfo info = worldCache.getPlayerInfo(playerID);

		// See if there are any objects to use...
		final List<GameObjectInfo> objects = worldCache.getObjects(info
				.getPosition().getMapID());
		for (final GameObjectInfo goi : objects) {
			final GameObject go = goi.toGameObject();

			if (!goi.isActive()) {
				continue;
			}

			if (!nearby(info.getPosition(), go.getPosition())) {
				continue;
			}

			list.add(goi.toInstruction(playerID));
		}

		// See if there is anyone to talk to...
		final List<NpcInfo> npcs = worldCache.getNpcs(info.getPosition()
				.getMapID());
		for (final NpcInfo ai : npcs) {
			if (nearby(info.getPosition(), ai.getPosition())) {
				list.addAll(canWeTalk(info, ai));
			}
		}

		return list;
	}

	List<GameEvent> instruction(final Instruction instruction) {
		final List<GameEvent> events = new ArrayList<GameEvent>();

		switch (instruction.getType()) {
		case MOVE_NORTH:
		case MOVE_SOUTH:
		case MOVE_EAST:
		case MOVE_WEST:
		case MOVE_FORWARD:
			movePlayer(instruction.getPlayerID(), instruction.getType(), events);
			break;
		case TURN_LEFT:
		case TURN_RIGHT:
			turnPlayer(instruction.getPlayerID(), instruction.getType(), events);
			break;
		case USE_MOVER:
			useMover(instruction.getPlayerID(), instruction.getFromID(), events);
			break;
		case START_CHAT:
			startChat(instruction.getPlayerID(), instruction.getStoryID(),
					instruction.getFromID(), events);
			break;
		case USE_OBJECT:
			useObject(instruction.getPlayerID(), instruction.getFromID(),
					events);
			break;
		case WEAR:
			wearObject(instruction.getPlayerID(), instruction.getFromID(),
					events);
		default:
			break;
		}

		return events;
	}

	private boolean isPlaceEmpty(final Position newPosition) {
		final ClientMap map = worldCache.getGameMap(newPosition.getMapID());
		final int y = (int) newPosition.getY();
		if (y < 0) {
			return false;
		} else if (y >= map.getRows().size()) {
			return false;
		}
		final int x = (int) newPosition.getX();
		if (x < 0) {
			return false;
		}
		final char[] chars = map.getRows().get(y).toCharArray();
		if (x >= chars.length) {
			return false;
		}

		if (chars[x] != '.') {
			return false;
		}

		// What about NPCs???
		for (final ActorInfo ai : map.getNpcs()) {
			final int npcX = (int) ai.getPosition().getX();
			if (npcX != x) {
				continue;
			}
			final int npcY = (int) ai.getPosition().getY();
			if (npcY == y) {
				return false;
			}
		}

		return true;
	}

	private GameEvent movePlayer(final String playerID,
			final Position newPosition) {
		final Position oldPosition = worldCache.updatePlayerPosition(playerID,
				newPosition);
		final PlayerInfo info = worldCache.getPlayerInfo(playerID);
		return GameEvent.changeLocation(newPosition, oldPosition,
				info.getFacing());
	}

	private void movePlayer(final String playerID, final Type type,
			final List<GameEvent> events) {
		PlayerInfo info = worldCache.getPlayerInfo(playerID);
		final Position newPosition = PositionCalculator.newPosition(
				info.getPosition(), info.getFacing(), type);

		if (isPlaceEmpty(newPosition)) {
			events.add(movePlayer(playerID, newPosition));

			// Have I set off any magic areas?
			info = worldCache.getPlayerInfo(playerID);
			final MagicArea magicArea = checkMagicAreas(newPosition,
					info.getMagicAreas());
			if (magicArea != null) {
				info.removeMagicArea(magicArea);
				runEffects(playerID, magicArea.getEffects(), events);
			}
		}
	}

	void runEffect(final String playerID, final GameEffect e,
			final List<GameEvent> events) {
		final PlayerInfo playerInfo = worldCache.getPlayerInfo(playerID);

		switch (e.getType()) {
		case PLAYER_MAP_CHANGE:
			events.add(movePlayer(playerID, e.getToPosition()));
			break;
		case START_CHAT:
			events.add(GameEvent.chatInstruction(e.getStoryID(), e.getFromID()));
			break;
		case OBJECT_ACTIVATE:
			events.add(worldCache.objectActive(e.getFromID(), true));
			break;
		case OBJECT_DEACTIVATE:
			events.add(worldCache.objectActive(e.getFromID(), false));
			break;
		case CHANGE_ACT:
			playerInfo.setAct(e.getStoryID(), e.getFromID());
			break;
		case INVENTORY_ADD:
			playerInfo.addToInventory(e.getFromID());
			break;
		case INVENTORY_REMOVE:
			playerInfo.removeFromInventory(e.getFromID());
			break;
		case NPC_MOVE:
			events.add(worldCache.updateNpcPosition(e.getFromID(),
					e.getToPosition()));
			break;
		case GENDER_CHANGE:
			final Gender g = Gender.valueOf(e.getToID());
			if ("Player".equals(e.getFromID())) {
				playerInfo.setGender(g);
				events.add(GameEvent.genderChange(e.getFromID(), g));
			} else {
				final NpcInfo npc = worldCache.getNpc(e.getFromID());
				npc.setGender(g);
				events.add(GameEvent.genderChange(e.getFromID(), g));
			}
			break;
		case TOKEN_ADD:
			playerInfo.addToken(e.getStoryID(), e.getFromID());
			break;
		default:
			throw new UnsupportedOperationException(e.toString());
		}
	}

	private void runEffects(final String player,
			final List<GameEffect> effects, final List<GameEvent> events) {
		for (final GameEffect e : effects) {
			runEffect(player, e, events);
		}
	}

	private void startChat(final String playerID, final String storyID,
			final String chatID, final List<GameEvent> events) {
		final PlayerInfo info = worldCache.getPlayerInfo(playerID);

		final DialogInfo dialogInfo = worldCache.getDialog(storyID, chatID);

		if (dialogInfo.isOnceOnly()) {
			info.setChatDone(storyID, chatID);
		}

		if (dialogInfo.getDialog().getText().length() > 0) {
			events.add(GameEvent.renderChat(storyID, chatID));
		}

		runEffects(playerID, dialogInfo.getEffects(), events);
	}

	List<GameEvent> startStory(final String playerID, final String storyID) {
		final Story story = worldCache.getStory(storyID);
		final PlayerInfo info = worldCache.getPlayerInfo(playerID);
		info.startStory(story);
		final List<GameEvent> events = new ArrayList<GameEvent>();
		runEffects(playerID, story.getStartEffects(), events);
		return events;
	}

	private void turnPlayer(final String playerID,
			final Instruction.Type direction, final List<GameEvent> events) {
		final PlayerInfo info = worldCache.getPlayerInfo(playerID);
		final Facing facing = PositionCalculator.newFacing(info.getFacing(),
				direction);
		info.setFacing(facing);
		events.add(GameEvent.changeLocation(info.getPosition(),
				info.getPosition(), facing));
	}

	private void useMover(final String playerID, final String moverID,
			final List<GameEvent> events) {
		final PlayerInfo info = worldCache.getPlayerInfo(playerID);
		final Collection<Mover> movers = worldCache.getMovers(info
				.getPosition().getMapID());
		for (final Mover m : movers) {
			if (m.getId().equals(moverID)) {
				events.add(movePlayer(playerID, m.getTo()));
				break;
			}
		}
	}

	private void useObject(final String playerID, final String objectID,
			final List<GameEvent> events) {
		final GameObjectInfo goi = worldCache.getObject(objectID);
		final List<GameEffect> effects = goi.getEffects();
		runEffects(playerID, effects, events);
	}

	private void wearObject(final String playerID, final String fromID,
			final List<GameEvent> events) {
		final PlayerInfo info = worldCache.getPlayerInfo(playerID);
		if (info.putOn(fromID)) {
			final GameObjectInfo goi = worldCache.getObject(fromID);
			runEffects(playerID, goi.getEffects(), events);
		}
	}
}
